Imports¶

  • Carregamento de bibliotecas
  • Configurações de apoio no notebook
  • Configurações de design gráficos
In [1]:
import numpy as np
import pandas as pd

import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px


import os
import warnings

import datetime

import missingno as msno
from tqdm import tqdm
In [2]:
warnings.filterwarnings('ignore')
In [3]:
mpl.rcParams['figure.figsize'] = (12,4)
mpl.rcParams['figure.dpi']     = 300
sns.set_style('darkgrid')
In [4]:
tqdm.pandas()

Contexto do problema¶

Você trabalha como Analista de Dados em uma empresa que fornece um serviço de rede social. O acesso ao serviço é concedido por meio de assinaturas mensais e os membros podem interagir por meio de comentários, respostas, adição de amigos e outras ações similares.

Alguns colaboradores perceberam uma alta evasão de membros da plataforma. Para validar tal hipótese, foi disponibilizado a você um conjunto de bases de dados que contempla informações sobre os assinantes e seus eventos na plataforma.

Você deve:

  • Validar a hipótese levantada pelos seus colegas
  • Montar uma estratégia para redução do churn com base nos dados fornecidos.
  • Esperamos um enfoque no negócio baseado em métricas e gráficos.

Carregamento de tabelas (Data Loading)¶

  • Definição de caminhos para os datasets
  • Carregamentos dos datasets utilizando Pandas
In [5]:
path_account      = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','account.csv')
path_event_type   = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','event_type.csv')
path_event        = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','event.csv')
path_subscription = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','subscription.csv')
In [6]:
account      = pd.read_csv(path_account)
event_type   = pd.read_csv(path_event_type)
event        = pd.read_csv(path_event)
subscription = pd.read_csv(path_subscription)

Diagrama de Tabelas¶

  • Diagrama de ligação entre as tabelas carregadas

Avaliação inicial dos dados¶

  • Visualização de informações sobre os dados
In [7]:
display(account.head()), account.info()
id channel date_of_birth country
0 1 appstore2 1948-09-10 CN
1 2 appstore1 1952-11-06 AU
2 3 appstore1 2002-11-10 GB
3 4 appstore1 1976-06-18 US
4 5 appstore1 1975-11-18 KR
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14641 entries, 0 to 14640
Data columns (total 4 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   id             14641 non-null  int64 
 1   channel        14641 non-null  object
 2   date_of_birth  14641 non-null  object
 3   country        13030 non-null  object
dtypes: int64(1), object(3)
memory usage: 457.7+ KB
Out[7]:
(None, None)
In [8]:
display(event_type.head()), account.info()
event_type_id event_type_name
0 0 post
1 1 newfriend
2 2 like
3 3 adview
4 4 dislike
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14641 entries, 0 to 14640
Data columns (total 4 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   id             14641 non-null  int64 
 1   channel        14641 non-null  object
 2   date_of_birth  14641 non-null  object
 3   country        13030 non-null  object
dtypes: int64(1), object(3)
memory usage: 457.7+ KB
Out[8]:
(None, None)
In [9]:
display(event.head()), event.info()
account_id event_time event_type_id
0 1 2020-01-06 03:20:00 0
1 1 2020-01-06 00:55:29 0
2 1 2020-01-06 10:16:34 1
3 1 2020-01-06 09:31:16 2
4 1 2020-01-06 20:40:21 2
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17746587 entries, 0 to 17746586
Data columns (total 3 columns):
 #   Column         Dtype 
---  ------         ----- 
 0   account_id     int64 
 1   event_time     object
 2   event_type_id  int64 
dtypes: int64(2), object(1)
memory usage: 406.2+ MB
Out[9]:
(None, None)
In [10]:
display(subscription.head()), subscription.info()
id account_id product start_date end_date mrr
0 0 1 standard 2020-01-06 2020-02-06 9.99
1 1 1 standard 2020-02-06 2020-03-06 9.99
2 2 1 standard 2020-03-06 2020-04-06 9.99
3 3 1 standard 2020-04-06 2020-05-06 9.99
4 4 1 standard 2020-05-06 2020-06-06 9.99
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 55502 entries, 0 to 55501
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   id          55502 non-null  int64  
 1   account_id  55502 non-null  int64  
 2   product     55502 non-null  object 
 3   start_date  55502 non-null  object 
 4   end_date    55502 non-null  object 
 5   mrr         55502 non-null  float64
dtypes: float64(1), int64(2), object(3)
memory usage: 2.5+ MB
Out[10]:
(None, None)
In [11]:
print('*'*5,'Table Shapes', '*'*5, '\n')
(print(f'Tabela account: \t{account.shape[1]} features, {account.shape[0]} registros'))
(print(f'Tabela event_type: \t{event_type.shape[1]} features, {event_type.shape[0]} registros'))
(print(f'Tabela event: \t\t{event.shape[1]} features, {event.shape[0]} registros'))
(print(f'Tabela subscription: \t{subscription.shape[1]} features, {subscription.shape[0]} registros'))
***** Table Shapes ***** 

Tabela account: 	4 features, 14641 registros
Tabela event_type: 	2 features, 8 registros
Tabela event: 		3 features, 17746587 registros
Tabela subscription: 	6 features, 55502 registros

Pré-processamento dos dados (Data preprocessing)¶

  1. Correção dos tipos de dados de features
  2. Identificação de valores missing
  3. Identificação de registros duplicados

Estruturação dos dados (Data Structuring)¶

  • Correção dos tipos de dados em cada tabela

Account Table¶

In [12]:
account.channel.unique()
Out[12]:
array(['appstore2', 'appstore1', 'web'], dtype=object)
In [13]:
account.country.unique()
Out[13]:
array(['CN', 'AU', 'GB', 'US', 'KR', 'IT', 'MX', 'CO', 'JP', nan, 'BR',
       'DE', 'ES', 'RU', 'AR', 'NO', 'CH', 'SE', 'IN', 'CA', 'ID', 'FR',
       'DK', 'GR', 'NZ', 'PT', 'NL'], dtype=object)
In [14]:
account.id.unique()
Out[14]:
array([    1,     2,     3, ..., 14639, 14640, 14641])
In [15]:
account['channel']       = account['channel'].astype('category')
account['country']       = account['country'].astype('category')
account['date_of_birth'] = pd.to_datetime(account['date_of_birth'], format='%Y-%m-%d')
In [16]:
account.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14641 entries, 0 to 14640
Data columns (total 4 columns):
 #   Column         Non-Null Count  Dtype         
---  ------         --------------  -----         
 0   id             14641 non-null  int64         
 1   channel        14641 non-null  category      
 2   date_of_birth  14641 non-null  datetime64[ns]
 3   country        13030 non-null  category      
dtypes: category(2), datetime64[ns](1), int64(1)
memory usage: 258.9 KB

Event Table¶

In [17]:
event.columns
Out[17]:
Index(['account_id', 'event_time', 'event_type_id'], dtype='object')
In [18]:
event.event_type_id.unique()
Out[18]:
array([0, 1, 2, 3, 6, 7, 4, 5])
In [19]:
event.account_id.unique()
Out[19]:
array([    1,     2,     3, ..., 14639, 14640, 14641])
In [20]:
event['event_type_id'] = event['event_type_id'].astype('category')
event['event_time']    = pd.to_datetime(event['event_time'], format='%Y-%m-%d %H:%M:%S')
event['account_id']    = event['account_id'].astype('category')
In [21]:
event.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17746587 entries, 0 to 17746586
Data columns (total 3 columns):
 #   Column         Dtype         
---  ------         -----         
 0   account_id     category      
 1   event_time     datetime64[ns]
 2   event_type_id  category      
dtypes: category(2), datetime64[ns](1)
memory usage: 186.8 MB

Event Type Table¶

In [22]:
event_type.columns
Out[22]:
Index(['event_type_id', 'event_type_name'], dtype='object')
In [23]:
event_type.event_type_id.unique()
Out[23]:
array([0, 1, 2, 3, 4, 5, 6, 7])
In [24]:
event_type.event_type_name.unique()
Out[24]:
array(['post', 'newfriend', 'like', 'adview', 'dislike', 'unfriend',
       'message', 'reply'], dtype=object)
In [25]:
event_type.event_type_id   = event_type.event_type_id.astype('category')
event_type.event_type_name = event_type.event_type_name.astype('category')

A tabela apresenta apenas valores únicos e de acordo com o tipo já atribuído, portanto, não há necessidade de conversão de tipos.

Subscription Table¶

In [26]:
subscription.head()
Out[26]:
id account_id product start_date end_date mrr
0 0 1 standard 2020-01-06 2020-02-06 9.99
1 1 1 standard 2020-02-06 2020-03-06 9.99
2 2 1 standard 2020-03-06 2020-04-06 9.99
3 3 1 standard 2020-04-06 2020-05-06 9.99
4 4 1 standard 2020-05-06 2020-06-06 9.99
In [27]:
subscription['product'].unique()
Out[27]:
array(['standard'], dtype=object)
In [28]:
subscription['mrr'].unique()
Out[28]:
array([9.99])

Há apenas um único tipo de produto vendido a um único preço.

In [29]:
subscription['account_id'].unique()
Out[29]:
array([    1,     2,     3, ..., 14639, 14640, 14641])
In [30]:
subscription['start_date']    = pd.to_datetime(subscription.start_date, format='%Y-%m-%d')
subscription['end_date']      = pd.to_datetime(subscription.end_date, format='%Y-%m-%d')

subscription['product']       = subscription['product'].astype('category')
subscription['account_id']    = subscription['account_id'].astype('category')
In [31]:
subscription.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 55502 entries, 0 to 55501
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype         
---  ------      --------------  -----         
 0   id          55502 non-null  int64         
 1   account_id  55502 non-null  category      
 2   product     55502 non-null  category      
 3   start_date  55502 non-null  datetime64[ns]
 4   end_date    55502 non-null  datetime64[ns]
 5   mrr         55502 non-null  float64       
dtypes: category(2), datetime64[ns](2), float64(1), int64(1)
memory usage: 2.5 MB

Valores Missing¶

  • Identificação de valores faltantes em cada tabela
In [32]:
account.isna().sum()
Out[32]:
id                  0
channel             0
date_of_birth       0
country          1611
dtype: int64
In [33]:
event_type.isna().sum()
Out[33]:
event_type_id      0
event_type_name    0
dtype: int64
In [34]:
event.isna().sum()
Out[34]:
account_id       0
event_time       0
event_type_id    0
dtype: int64
In [35]:
subscription.isna().sum()
Out[35]:
id            0
account_id    0
product       0
start_date    0
end_date      0
mrr           0
dtype: int64

Apenas a tabela account apresenta valores faltantes. São 1611 valores faltantes na coluna do país de origem dos clientes.

In [36]:
(account.country.value_counts())
Out[36]:
CN    2241
US    2153
JP     740
BR     725
IN     631
CA     613
GB     595
DE     592
AU     576
MX     523
KR     450
AR     430
FR     417
NZ     318
CO     288
RU     270
ID     159
DK     158
NL     155
ES     153
GR     150
NO     149
IT     143
CH     137
SE     137
PT     127
Name: country, dtype: int64
In [37]:
msno.matrix(account, figsize = (16,5))
Out[37]:
<AxesSubplot:>

Como são muitos valores únicos na feature e os valores faltantes estão uniformemente distribuídos no dataset, não é possível fazer alguma dedução segura para uma imputação imediata. O tratamento será feito de acordo com a situação que a variável for trabalhada.

Duplicatas¶

  • Identificação de valores duplicadas nas tabelas
In [38]:
print(f'Duplicatas em Event: {event.duplicated().sum()}')
print(f'Duplicatas em Account: {account.duplicated().sum()}')
print(f'Duplicatas em Subscription: {subscription.duplicated().sum()}')
print(f'Duplicatas em Event_Type: {event_type.duplicated().sum()}')
Duplicatas em Event: 1296
Duplicatas em Account: 0
Duplicatas em Subscription: 0
Duplicatas em Event_Type: 0
In [39]:
event.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17746587 entries, 0 to 17746586
Data columns (total 3 columns):
 #   Column         Dtype         
---  ------         -----         
 0   account_id     category      
 1   event_time     datetime64[ns]
 2   event_type_id  category      
dtypes: category(2), datetime64[ns](1)
memory usage: 186.8 MB

Considerando que a coluna event_time da tabela event dá a precisão em segundos da ocorrência do evento, pode-se considerar as linhas duplicadas nesta tabela como realmente um registro duplo da mesma ocorrência. Sendo assim, portanto, é razoável que sejam retiradas da análise

In [40]:
event.drop_duplicates(inplace = True)
In [41]:
# Preservar RangeIndex para redução do uso de memória
event.reset_index(drop = True, inplace = True)
In [42]:
print(f'Duplicatas em Event: {event.duplicated().sum()}')
print(f'Duplicatas em Account: {account.duplicated().sum()}')
print(f'Duplicatas em Subscription: {subscription.duplicated().sum()}')
print(f'Duplicatas em Event_Type: {event_type.duplicated().sum()}')
Duplicatas em Event: 0
Duplicatas em Account: 0
Duplicatas em Subscription: 0
Duplicatas em Event_Type: 0
In [43]:
event.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17745291 entries, 0 to 17745290
Data columns (total 3 columns):
 #   Column         Dtype         
---  ------         -----         
 0   account_id     category      
 1   event_time     datetime64[ns]
 2   event_type_id  category      
dtypes: category(2), datetime64[ns](1)
memory usage: 186.8 MB
In [44]:
event.head()
Out[44]:
account_id event_time event_type_id
0 1 2020-01-06 03:20:00 0
1 1 2020-01-06 00:55:29 0
2 1 2020-01-06 10:16:34 1
3 1 2020-01-06 09:31:16 2
4 1 2020-01-06 20:40:21 2

Análise Exploratória de Dados (EDA)¶

Questões levantadas:

  1. Qual a extensão do período a ser analisado?
  2. Quantas assinaturas foram realizadas em cada mês?
  3. Qual o número de interações registradas em cada mês?
  4. Para entender como os usuários interagem com a plataforma previamente ao abandono do serviço, qual o nível de cada uma das possíveis interações em cada mês?
  5. Quantas assinaturas são renovadas e quantas são iniciadas a cada mês?
  6. Quantas assinaturas são realizadas em cada um dos canais de assinatura em cada mês?
  7. Qual a faixa etária dos usuários da plataforma?
  8. Por quanto tempo (meses) cada um dos assinantes permaneceu na plataforma ? Qual a faixa etária de cada grupo que permaneceu durante certo período?
  9. Há alguma diferença no tipo de interação feita pelos usuários a depender da sua faixa etária?
  10. De que país são os assinantes da plataforma?
  11. Há alguma variação no nível de interação dos usuários em algum momento do mês? Há alguma variação na quantidade de assinaturas realizadas em algum momento do mês?
  12. Quais dias da semana dão o maior pico de interações na rede?
  13. Há algum momento do dia em que o número de interações se intensifica?
  14. Qual a faixa etária dos usuários em cada país?
  15. Quais canais de assinatura são mais utilizados em cada faixa etária?
  16. Qual o perfil dos usuários que deixaram o serviço em junho?
  17. Quantos usuários "churnaram" em cada mês?

Qual o período coberto pela base de dados?¶

In [45]:
print(f'Início: {event.event_time.min()} \nFim: \t{event.event_time.max()}')
Início: 2020-01-01 00:00:01 
Fim: 	2020-06-30 23:59:59

O período analisado ocorre do início do ano de 2020, em janeiro até o término do mês de junho.

In [52]:
meses = dict(zip(np.arange(1,7), ['Jan','Fev','Mar','Abr','Mai','Jun']))

Volume mensal de assinaturas¶

In [46]:
account_subscription = account.merge(subscription, how = 'left', left_on = 'id', right_on = 'account_id').drop(['id_y', 'id_x'], axis = 1)
In [47]:
account_subscription['start_month'] = account_subscription['start_date'].dt.month
account_subscription['start_day']   = account_subscription['start_date'].dt.day

account_subscription['end_month']   = account_subscription['end_date'].dt.month
account_subscription['end_day']     = account_subscription['end_date'].dt.month
In [48]:
((account_subscription.end_date - account_subscription.start_date).value_counts())
Out[48]:
31 days    32549
30 days    12305
29 days    10648
dtype: int64

Todas as assinaturas são de duração de 1 mês.

In [49]:
account_subscription.head()
Out[49]:
channel date_of_birth country account_id product start_date end_date mrr start_month start_day end_month end_day
0 appstore2 1948-09-10 CN 1 standard 2020-01-06 2020-02-06 9.99 1 6 2 2
1 appstore2 1948-09-10 CN 1 standard 2020-02-06 2020-03-06 9.99 2 6 3 3
2 appstore2 1948-09-10 CN 1 standard 2020-03-06 2020-04-06 9.99 3 6 4 4
3 appstore2 1948-09-10 CN 1 standard 2020-04-06 2020-05-06 9.99 4 6 5 5
4 appstore2 1948-09-10 CN 1 standard 2020-05-06 2020-06-06 9.99 5 6 6 6
In [50]:
sig_month = account_subscription.groupby('start_month').count()['channel']
In [53]:
sns.lineplot(x         = sig_month.index,
             y         = sig_month.values, 
             palette   = 'Set1', 
             marker    = 'o', 
             color     = '#BA0000', 
             linestyle = '--')

plt.xlabel('Mês', fontsize = 12)
plt.ylabel('Assinaturas', fontsize = 12)
plt.xticks(np.arange(1,7), meses.values())
plt.title('Assinaturas realizadas ao mês', fontdict = {'fontsize':14, 'weight':'bold'})

plt.savefig('img/sign_month.png')
plt.show()

O serviço vinha numa tendência crescente de janeiro até maio. No entanto, há uma forte queda no número de assinaturas registradas no mês de junho.

Volume mensal de interações¶

In [55]:
event['month'] = event.event_time.dt.month
event.month    = event.month.astype('category')
In [56]:
month_groupby = event.groupby('month')
In [70]:
sns.lineplot(data = event.groupby(['account_id','month']).count().reset_index(), 
             x    = 'month', y = 'event_time', 
             marker = 'o', 
             linewidth = 2, 
             linestyle = '--')

plt.xticks(ticks = np.arange(1,7), labels = ['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.ylabel('Interações por usuário', fontsize = 12)
plt.xlabel('Mês', fontsize = 12)
plt.title('Média de interações por usuários por mês', fontdict = {'fontsize':14, 'weight':'bold'})
plt.tight_layout()

plt.savefig('img/int_month.png')
plt.show()
  • O número de interações com a plataforma vinha numa crescente até o mês de maio onde atingiu seu pico. No mês de junho houve uma redução, sendo o menor número de interações registrados no mês desde o mês de janeiro.
  • A margem de erro em cada mês é pequena, portanto, o valor é similar para todos os usuários da plataforma.
  • O resultado está de acordo com o que foi observado no volume mensal de assinaturas que revelou uma forte queda no número de assinantes. Consequentemente, menos interações seriam registradas.

Quantidade possíveis interações em cada mês¶

In [51]:
grouped_interactions = event.groupby('event_type_id')

for interaction, group in grouped_interactions:
    
    ax = sns.lineplot(x         = group.month.value_counts().index, 
                      y         = group.month.value_counts().values, 
                      label     = interaction, 
                      marker    = '^', 
                      linewidth = 2, 
                      linestyle = '--')

plt.title('Número de interações por mês por categoria', weight = 'bold', fontsize = 14)
ax.set_ylabel('Interações',fontsize=12)
ax.set_xlabel('Mês',fontsize=12)

ax.legend(event_type.event_type_name.unique(), title = 'Interação')
ax.set_xticks(np.arange(1,7), meses.values())
plt.tight_layout()
plt.show()

A proporção entre as diferentes formas de interação possíveis permaneceu relativamente constante durante os meses, o que indica que o comportamento dos usuários não variou durante o período, mesmo no momento em que pararam de renovar a assinatura na rede.

Assinaturas Novas x Assinaturas Renovadas¶

In [58]:
# Coluna que define se assinatura é nova ou renovada
account_subscription['renew'] = account_subscription.progress_apply(lambda x: x.account_id in (account_subscription[account_subscription.start_month == (x.start_month-1)]['account_id']).values, axis = 1)
100%|████████████████████████████████████| 55502/55502 [02:51<00:00, 323.74it/s]
In [59]:
account_subscription.sort_values(by='account_id').head()
Out[59]:
channel date_of_birth country account_id product start_date end_date mrr start_month start_day end_month end_day renew
0 appstore2 1948-09-10 CN 1 standard 2020-01-06 2020-02-06 9.99 1 6 2 2 False
1 appstore2 1948-09-10 CN 1 standard 2020-02-06 2020-03-06 9.99 2 6 3 3 True
2 appstore2 1948-09-10 CN 1 standard 2020-03-06 2020-04-06 9.99 3 6 4 4 True
3 appstore2 1948-09-10 CN 1 standard 2020-04-06 2020-05-06 9.99 4 6 5 5 True
4 appstore2 1948-09-10 CN 1 standard 2020-05-06 2020-06-06 9.99 5 6 6 6 True
In [60]:
group_channels = account_subscription.groupby(['start_month','channel']).count()['product'].reset_index()
In [242]:
sns.countplot(data = account_subscription, 
              x    = 'start_month', 
              hue  = 'renew')

plt.legend(labels     = ['Nova Assinatura','Renovação'], title = 'Evento')
plt.xticks(ticks  = np.arange(0,6), labels=['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.xlabel('Mês', fontsize = 12)
plt.ylabel('Assinaturas', fontsize = 12)
plt.title('Novas Assinaturas x Renovações',fontsize = 14, weight = 'bold')

plt.savefig('img/renews.png')

plt.show()
  • Como não temos dados anteriores, consideramos as assinaturas do mês de Janeiro como novas assinaturas. É possível constatar que a a maioria das assinaturas ativas vem de renovações de usuários e uma pequena fração incremental de novas assinaturas a cada mês. No último mês, no entanto, não houveram novas assinaturas e a taxa de renovação foi muito abaixo do que vinha sendo observado.

Volume de assinaturas por canal¶

In [ ]:
 
In [75]:
sns.lineplot(data   = account_subscription.groupby(['start_month','channel']).count()['product'].reset_index(), 
             x      ='start_month', 
             y      ='product', 
             hue    = 'channel', 
             marker = 'o')

plt.title('Assinaturas por canal', weight = 'bold', fontsize = 14)
plt.xlabel('Mês', fontsize = 12)
plt.ylabel('Assinaturas', fontsize = 12)
plt.xticks(np.arange(1,7), meses.values())
plt.legend(title = 'Canal de Assinatura')

plt.savefig('img/channels.png')

plt.show()
  • O canal appstore2 é o mais utilizado pelos usuários seguido pela appstore1 e web.
  • A quantidade de assinantes vindos de cada appstore permanece estável no decorrer do tempo.
  • O destaque maior para a appstore2 pode indicar um perfil socioeconômico para o tipo de cliente que se torna-se assinante da rede.

Avaliação de Faixa Etária¶

In [77]:
account_subscription['age'] = account_subscription.date_of_birth.progress_apply(lambda x: datetime.date.today() - x.date())
100%|█████████████████████████████████| 55502/55502 [00:00<00:00, 129353.47it/s]
In [78]:
account_subscription['age'] = round(account_subscription.age.dt.days/365,0)
In [81]:
account_subscription.age.min(), account_subscription.age.max()
Out[81]:
(14.0, 85.0)
In [64]:
sns.histplot(account_subscription.age)
plt.title('Distribuição de assinantes por idade', fontdict = {'fontsize':14, 'weight':'bold'})
plt.xlabel('Idade', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.show()

O gráfico de idades mostra que há uma distribuição picos em diferentes faixas de idade. Para facilitar a análise, será feito o agrupamento das idades em faixas etárias.

Considerando:

  • Até 19 anos: Adolescentes e jovens
  • Entre 20 e 39 anos: Jovens adultos
  • Entre 40 e 59 anos: Adultos
  • Entre 60 e 79: Idosos
  • Acima de 80: Idosos+
In [65]:
account_subscription['age_group'] = pd.cut(account_subscription.age, bins=[0,19,39,59,79, 100], labels=['Adolescentes e Jovens','Jovens Adultos','Adultos','Idosos', 'Idosos+'])
In [244]:
colors = sns.color_palette('pastel')[0:5]
plt.figure(figsize = (4,2))
explode = [0.01, 0.01, 0.01, 0.01, 0.01]

plt.pie(account_subscription.drop_duplicates(['account_id'])['age_group'].value_counts().values, 
        labels    = account_subscription.drop_duplicates(['account_id'])['age_group'].value_counts().index, 
        colors    = colors, 
        autopct   ='%.0f%%', 
        explode   = explode,
        textprops =  {'fontsize': 5})

plt.title('Distribuição do Público por Faixa Etária',fontsize= 8, weight = 'bold')

plt.savefig('img/age_groups.png')

plt.show()

Principais públicos dividem-se igualmente entre jovens adultos, adultos e idosos, chegando a somar aprox. 85% do público total.

Período total de assinatura por conta¶

In [68]:
ltv = subscription.groupby('account_id').agg({'start_date':'min', 'end_date':'max'})
In [69]:
ltv['period'] = (ltv.progress_apply(lambda x: int((x.end_date - x.start_date).days/30), axis = 1)).astype('category')
100%|███████████████████████████████████| 14641/14641 [00:04<00:00, 3623.36it/s]
In [245]:
sns.histplot(data=ltv, x='period')
plt.title('Distribuição do período de assinatura por cliente', fontdict = {'fontsize':14, 'weight':'bold'})
plt.xlabel('Tempo de assinatura (meses)', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)

plt.savefig('img/sign_period.png')

plt.show()

A maior parte dos clientes assinou o serviço durante 5 dos 6 meses avaliados. O fato pode indicar que há algum nível de fidelidade do cliente com a rede.

In [71]:
ltv['age_group'] = (ltv.index.map(dict(zip(account_subscription.account_id,account_subscription.age_group)))).astype('category')
In [72]:
age_hue_order = ['Adolescentes e Jovens','Jovens Adultos','Adultos','Idosos','Idosos+']
In [73]:
sns.countplot(data      = ltv, 
              x         = 'period', 
              hue       = 'age_group',
              hue_order = age_hue_order)

plt.xlabel('Tempo de assinatura (meses)', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Período de assinatura em cada faixa etária', fontdict = {'fontsize':14, 'weight':'bold'})
plt.legend(title = 'Faixa Etária')
plt.show()

Não parece haver variação significativa na proporção de assinantes por grupo etário considerando todos os períodos de assiantura.

In [74]:
ltv_event = ltv.merge(event, how='right', 
                      left_index = True, 
                      right_on = 'account_id')

Tipos de interações por faixa etária¶

In [75]:
interaction_hue_order = [2,6,0,3,7,4,1,5]
interaction_legend = ['like','message','post','adview','reply','dislike','newfriend','unfriend']
In [76]:
sns.countplot(data      = ltv_event[['age_group','event_type_id']], 
              x         = 'age_group',  
              hue       = 'event_type_id',
              hue_order = interaction_hue_order)

plt.xlabel('Faixa Etária', fontsize=12)
plt.ylabel('Interações', fontsize=12)
plt.title('Tipos de interações registradas entre faixas etárias', fontdict = {'fontsize':14, 'weight':'bold'})
plt.legend(interaction_legend, title = 'Interação')
plt.show()

A proporção de interações praticadas pelos usuários de cada grupo etário parece não variar, indicando que os usuários parecem interagir com a plataforma de maneira semelhante, independente da sua idade.

Avaliação de usuários por país¶

In [77]:
temp = account.country.value_counts().sort_values(ascending = False)

sns.barplot(x     = temp.index, 
            y     = temp, 
            order = temp.index)

plt.xlabel('País', fontsize=12)
plt.ylabel('Assinantes', fontsize=12)
plt.title('Volume de assinaturas por país no período', fontdict = {'fontsize':14, 'weight':'bold'})
plt.tight_layout()
plt.show()

China e Estados Unidos constituem a maior fração do público assinante do serviço.

In [78]:
grouped_countries = [account_subscription[account_subscription.country==ctr] for ctr in account.country.unique() if ctr is not np.nan]
In [247]:
#Cores para a as séries temporais de cada país no gráfico abaixo

colors = ['#800000', '#FF4500','#CD5C5C','#FFD700', '#B8860B', 
          '#EEE8AA', '#9ACD32','#7CFC00','#006400','#8B4513', 
          '#000000', '#8FBC8F', '#00FA9A','#2E8B57','#2F4F4F',
         '#008080','#00FFFF','#AFEEEE','#4682B4','#191970',
         '#0000FF','#8A2BE2','#4B0082', '#8B008B','#FF00FF','#F5DEB3']
In [248]:
for n, country in tqdm(enumerate(grouped_countries)):
    cur_country = country.groupby('start_month').count()['country']
    
    ax = sns.lineplot(x         = cur_country.index, 
                      y         = cur_country, 
                      marker    = '*', 
                      label     = country.country.iloc[0], 
                      color     = colors[n], 
                      linestyle = '-')
    
handles, labels = ax.axes.get_legend_handles_labels()
ax.legend_.remove()
ax.figure.legend(handles, labels, ncol=7, loc='upper left', 
                bbox_to_anchor=(0.5, 1.15), frameon=True, title = 'País')

ax.set_xlabel('Mês', fontsize = 12)
ax.set_ylabel('Assinaturas', fontsize = 12)
ax.set_xticks(np.arange(1,7), labels = ['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.title('Variação de assinaturas por país', loc = 'left', fontdict = {'fontsize':14, 'weight':'bold'})
plt.tight_layout()

plt.savefig('img/sign_month_countries.png')

plt.show()
26it [00:01, 23.66it/s]

Todos os países seguiram a mesma tendência e tiveram reduzido o número de assinantes da rede no mês de junho, o que indica que o churn observado foi um fenômeno mundial.

Avaliação de interações por dia¶

In [81]:
event_ts = event.set_index('event_time').resample('D').count()
In [82]:
subscription_ts = subscription.set_index('start_date').resample('D').count()
In [83]:
subscription_ts.loc['2020-06-02',:] = [0,0,0,0,0] 
subscription_ts.loc['2020-06-30',:] = [0,0,0,0,0] 
In [252]:
f, ax = plt.subplots(nrows = 2, sharex = True)

ax[0] = event_ts['account_id'].plot(linewidth=2, ax = ax[0])
ax[0].set_ylabel('Interações', fontsize = 12)
ax[0].set_xlabel('Mês', fontsize = 12)
ax[0].set_title('Interações diárias', fontsize = 14, weight = 'bold')


ax[1] = subscription_ts['account_id'].plot(linewidth=2, ax = ax[1])
ax[1].set_ylabel('Assinaturas', fontsize = 12)
ax[1].set_xlabel('Mês', fontsize = 12)
ax[1].set_title('Assinaturas diárias', fontsize = 14, weight = 'bold')
ax[1].set_xlim(['2020-01-01','2020-06-30'])
plt.tight_layout()

ax[0].axvline(x='2020-05-31', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-04-30', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-03-31', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-02-29', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-01-31', color = 'black', linestyle = '--')

ax[1].axvline(x='2020-04-30', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-03-31', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-02-29', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-01-31', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-05-31', color = 'black', linestyle = '--')

plt.savefig('img/daily_int_sign.png')


plt.show()
  • O fluxo de interações não tem uma sazonalidade específica. Portanto não parece haver um período do mês onde as interações na plataforma são mais intensas.
  • A partir do mês de junho as interações diárias na plataforma passam a cair rapidamente.
  • Ao final de cada mês, a partir de fevereiro, é possível observar que há um pico no volume de assinaturas.
  • A partir do mês de junho, o único dia em que são registradas novas assinaturas é o dia 01. Em seguida nenhuma nova assinatura foi registrada. O fato pode ser resultado de uma evasão forte dos clientes ou indisponibilidade da plataforma para renovação das assinaturas, especialmente no fim do mês, quando há um pico no número de renovações registradas nos meses anteriores.

Avaliação por dia da semana¶

In [85]:
event_ts['dow'] = event_ts.index.dayofweek
In [86]:
dow = ['dom','seg','ter','qua','qui','sex','sab']
In [250]:
dow_events =event_ts.groupby('dow').mean()['account_id']
dow_events.index = dow
dow_events = dow_events.sort_values(ascending = False)
sns.barplot(y=dow_events.index, x = dow_events.values, orient = 'h')
plt.title('Média de interações por dia da semana', weight = 'bold', fontsize = 14)
plt.xlabel('Interações', fontsize = 12)
plt.ylabel('Dias da semana', fontsize = 12)

plt.savefig('img/dow_int.png')


plt.show()

Os dias com maior concentração de interações na plataforma são quinta-feira, sexta-feira e sábado. O resultado faz sentido já que a maioria do público da rede é adulto e provavelmente tem mais tempo de interagir com o conteúdo da rede nos fins de semana.

Avaliação por horário¶

In [88]:
hour_event = event[['account_id','event_time']].set_index('event_time').resample('H').count()
In [89]:
hour_event['hour'] = hour_event.index.hour
In [251]:
(hour_event.groupby('hour').mean()['account_id']).plot(kind='line', marker = 'd', color = '#006400')
plt.xlim([0,23])
plt.xticks(np.arange(0,24,4))
plt.title('Média de interações por faixa horária', weight = 'bold', fontsize = 14)
plt.xlabel('Hora', fontsize = 12)
plt.ylabel('Interações', fontsize = 12)

plt.axvline(x=6, linestyle = '--', color = 'black')
plt.axvline(x=12, linestyle = '--', color = 'black')
plt.axvline(x=18, linestyle = '--', color = 'black')
plt.text(x=1.75,y=4070, s='Madrugada', fontdict={'weight':'bold'})
plt.text(x=8.25,y=4070, s='Manhã', fontdict={'weight':'bold'})
plt.text(x=14.5,y=4070, s='Tarde', fontdict={'weight':'bold'})
plt.text(x=20,y=4070, s='Noite', fontdict={'weight':'bold'})


plt.savefig('img/hour_int.png')

plt.show()
In [91]:
print('O gráfico mostra que o período de pico de interações com maior estabilidade ocorre durante a tarde, das 12:00 às 18:00.')
print(f"No entanto, a média de interações por horário, em geral, é relativamente constante ficando entre {round(hour_event.groupby('hour').mean()['account_id'].min(),0)} e {round(hour_event.groupby('hour').mean()['account_id'].max(),0)} interações por hora.")
O gráfico mostra que o período de pico de interações com maior estabilidade ocorre durante a tarde, das 12:00 às 18:00.
No entanto, a média de interações por horário, em geral, é relativamente constante ficando entre 4057.0 e 4071.0 interações por hora.

Perfil dos países por grupo etário e canal de assinatura¶

In [92]:
age_country = account_subscription.groupby(['country','age_group'])['age'].count().reset_index()
In [93]:
sns.barplot(data = age_country, 
            x    = 'country',
            y    = 'age',
            hue  = 'age_group')

plt.xlabel('País', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Faixa etária de assinantes por país', fontsize = 14, weight = 'bold')
plt.legend(title = 'Faixa Etária')
plt.show()

Não há variação nas proporções de grupos etários que assinam o serviço em cada país.

In [94]:
channel_country = account_subscription.groupby(['country','channel'])['age'].count().reset_index()
In [95]:
sns.barplot(data = channel_country, 
            x    = 'country',
            y    = 'age',
            hue  = 'channel')

plt.xlabel('País', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Canais de assinatura utilizados por país', fontsize = 14, weight = 'bold')
plt.legend(title = 'Canal de Assinatura')

plt.show()

A escala preferência pelos canais de assinatura são as mesmas em todos os países onde o serviço é disponibilizado.

Canais de assinatura por faixa etária¶

In [96]:
channel_age = account_subscription.groupby(['age_group','channel'])['age'].count().reset_index()
In [97]:
sns.barplot(data = channel_age, 
            x    = 'age_group',
            y    = 'age',
            hue  = 'channel')

plt.xlabel('Faixa Etária', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Canais de assinatura utilizados por faixa etária', fontsize = 14, weight = 'bold')
plt.legend(title = 'Canal de Assinatura')
plt.show()

A preferência entre os canais de assinatura também é a mesma para todos os grupos etários.

Quem são os clientes que renovaram a assinatura em julho?¶

Canais de assinatura utilizados¶

In [98]:
clients_july = account_subscription[account_subscription['end_month'] == 7]
In [99]:
(clients_july.channel.value_counts()).plot(kind='barh')
plt.title('Canais de assinatura (assinantes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Assinantes',fontsize = 12)
plt.ylabel('Canais de assinatura',fontsize = 12)
plt.show()

Faixa etária¶

In [100]:
(clients_july.age_group.value_counts()).plot(kind='barh')
plt.title('Faixa etária (assinantes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Assinantes',fontsize = 12)
plt.ylabel('Faixa etária',fontsize = 12)
plt.show()

Histórico de interações com a plataforma¶

In [101]:
event_clients_july = event[event.account_id.isin(clients_july.account_id.values)]
In [102]:
ecj_groupby = (event_clients_july.groupby(['month','event_type_id']).count()).reset_index()
In [103]:
sns.lineplot(data   =  ecj_groupby, 
             x      = 'month',
             y      = 'account_id',
             hue    = 'event_type_id', 
             marker = 'o')

plt.legend(event_type.event_type_name.unique().tolist(), loc='upper left')
plt.title('Histórico de interações (assinantes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Mês',fontsize = 12)
plt.ylabel('Assinantes',fontsize = 12)
plt.xticks(np.arange(1,7), meses.values())
plt.show()

Em geral, o perfil e o comportamento dos usuários que deixaram de assinar a rede em junho é o mesmo que vinha já sendo observado nas análises mais gerais de perfil e comportamento.

Tempo que utilizam a plataforma¶

In [104]:
ax = ltv.loc[clients_july.account_id.values,:].period.value_counts().plot(kind='bar')
ax.bar_label(ax.containers[0])
plt.title('Período de assinatura total (clientes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Período (meses)',fontsize = 12)
plt.ylabel('Assinantes',fontsize = 12)
plt.xticks(rotation = 0)
plt.show()

A maior parte dos assinantes do mês de junho/julho são utilizam a plataforma há, pelo menos, 6 meses.

Não há evidências fortes, pelos dados fornecidos, que os clientes que ainda renovaram sua assinatura para o mês de junho até julho sejam diferentes dos demais clientes que não renovaram suas assinaturas.

Nível de evasão mensal¶

In [149]:
sub1 = subscription.sort_values(by=['account_id','start_date'])
In [150]:
sub1['next_'] = sub1.account_id.shift(-1)
In [151]:
sub1['churn'] = sub1.account_id != sub1.next_
sub1.churn = sub1.churn.astype('int').astype('category')
In [152]:
sub1['month'] = sub1.start_date.dt.month
In [153]:
sub1 = sub1[['account_id','month','churn']]
In [154]:
sub1.head()
Out[154]:
account_id month churn
0 1 1 0
1 1 2 0
2 1 3 0
3 1 4 0
4 1 5 1
In [253]:
pct = sub1.groupby(['month','churn']).count().reset_index()
no_churn_pct = np.around((pct[pct.churn==0]['account_id'].values/(pct[pct.churn==1]['account_id'].values + pct[pct.churn==0]['account_id'].values))*100,2)
churn_pct = np.around((pct[pct.churn==1]['account_id'].values/(pct[pct.churn==1]['account_id'].values + pct[pct.churn==0]['account_id'].values))*100,2)

ax = sns.countplot(data = sub1, 
                   x    = 'month',
                   hue  = 'churn')

plt.title('Churn de assinantes por mês', weight = 'bold', fontsize = 12)
plt.xlabel('Mês',fontsize = 12)
plt.ylabel('Assinantes',fontsize = 12)
ax.set_xticks(np.arange(0,6), labels = ['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.legend(['Não','Sim'], title = 'Churn', loc = 'upper right')

ax.bar_label(ax.containers[0], labels = no_churn_pct)
ax.bar_label(ax.containers[1], labels = churn_pct)

plt.savefig('img/churn.png')


plt.show()
  • A tabela mostra o número de assinantes que renovaram, ou não, a assinatura na rede para o próximo mês.
  • A taxa de churn até abril vinha em torno de de 4.5% e 6.76% do total de assinantes. No mês de maio, mais de 97% das assinaturas ativas não foram renovadas para o mês seguinte.

Conclusões¶

  • A plataforma possui apenas um produto, com um único período de serviço de assinatura e preço único. Ações como a adição de novos planos personalizados ao gosto do cliente, planos de assinatura mais longos (trimestrais, semestrais, anuais) ou planos familiares a preços com desconto, disponibilização de conteúdo exclusivo para modalidades de assinatura mais completas, diferentes produtos com uma ampla faixa de preços podem aumentar o interesse no uso da plataforma. Ainda, a empresa pode oferecer um período gratuito de acesso ao conteúdo para que novos clientes possam testar e avaliar o produto.

  • A maior parte do público assinante está na fase adulta. Por isso, a comunicação, aparência e conteúdos trazidos para a plataforma devem dialogar com esse público para consigam se identificar e estabelecer uma relação com o cliente, além de atrair com consistência o seu público alvo, que aprecia a proposta de valor do produto e tem menor probabilidade de deixar de consumi-lo. Outro fator observado nos dados é a preferência de grande parte dos assinantes pela utilização da appstore2 como canal de assinatura. Essa preferência revelar importantes características do perfil socioeconômico dos usuários da plataforma que também podem servir de apoio na elaboração de planos e serviços para retenção de clientes.

  • A queda simultânea nas assinaturas e interações em diversos países, sendo Estados Unidos e China os principais, pode indicar algum tipo de falha na manutenção dos serviços da plataforma que impediram que os assinantes continuassem utilizando a rede e suas assinaturas. Outra possibilidade é que nesse período tenha sido lançado no mercado um produto concorrente com características semelhantes e maior alinhamento com as necessidades dos clientes, que tenha puxado para si os clientes da plataforma. No entanto, a maior parte dos assinantes já vinha consumindo o produto desde o primeiro mês do período analisado, o que mostra um nível de fidelidade à plataforma. Nesse caso, a alta evasão seria um alerta para a necessidade de aprimorar a fidelização dos clientes com a plataforma.

  • A maior parte das atividades na plataforma parece ocorrer na véspera dos finais de semana, com um pico maior de interações na parte da noite. A plataforma pode aproveitar esses períodos para lançar novos eventos exclusivos na plataforma, buscando manter o assinante engajado.

  • A plataforma poderia iniciar um trabalho de entrar em contato com os clientes que não renovaram no último mês para levantar informações acerca do motivo para enriquecer a base de de dados e melhorar o serviço e a previsibilidade de churn.